"""Miscellaneous functions."""
import datetime
import time
import os
import re
import numpy as np
import sys
import logging
import functools
import logging
from PyQt4.QtGui import QMessageBox
from dateutil.relativedelta import relativedelta

import quoteperiod as qp

def enum(*sequential, **named):
    """"
    Simulation" of enumerated types.
    
    Returns a class with fields as specified in *sequential and/or **named
    
    Example:
    >> TickType = enum('Full', 'History', 'TimeStamp')
    >> TickType.Full
    0
    """
    enums = dict(zip(sequential, range(len(sequential))), **named)
    return type('Enum', (), enums)

##############################################################################
## Datetime routines

FMT = "%Y-%m-%d %H:%M" # Date format
FMTS = "%Y-%m-%d %H:%M:%S" # Date format with seconds

def dt2ts(dt):
    """Converts to float representing number of seconds since 1969-12-31 21h."""
    return int(time.mktime(dt.timetuple())) \
           if isinstance(dt, (datetime.datetime, datetime.date)) else int(dt)
    
def ts2dt(ts):
    return datetime.datetime.fromtimestamp(ts) \
           if not isinstance(ts, (datetime.datetime, datetime.date)) else ts

def dt2str(dt, flagSeconds=True):
    """Converts datetime object to str if not yet an str."""
    if isinstance(dt, str):
        return dt
    return dt.strftime(FMTS if flagSeconds else FMT)

def str2dt(s):
    """Works with time with/without seconds."""
    return datetime.datetime.strptime(s, FMTS if s.count(":") == 2 else FMT)

def ts2str(s):
    """Shortcut to dt2str(ts2dt(s))."""
    return dt2str(ts2dt(s))

def AddTimeIfZero(dt):
    """Adds time=23h59 to datetime object if time is not specified.
    
    Arguments:
      dt -- either a str or a datetime.datetime object. If string, dt needs to
           follow the "%Y-%m-%d %H:%M" or "%Y-%m-%d" format.
    
    Returns:
      datetime.datetime or str, according to d.
    
    """
    bIsStr = isinstance(dt, str)
    if bIsStr:
        if dt.find(" ") > -1:
            dt = datetime.datetime.strptime(dt, "%Y-%m-%d %H:%M")
        else:
            dt = datetime.datetime.strptime(dt, "%Y-%m-%d")
    if dt.hour == 0 and dt.minute == 0:
        dt = dt.replace(hour=23, minute=59)
    if bIsStr:
        return dt.strftime("%Y-%m-%d %H:%M")
    return dt

def EndOfToday():
    return EndOfDay(datetime.datetime.today())

def EndOfDay(dt):
    """Sets hour to 23h59:00.0. dt is datetime.
    
    Note: this is an important definition. End of a day is defined as 23h59:00
    (seconds set to zero). I hope this won't be a problem if I decide to watch
    the ForEx market.
    """
    return dt.replace(hour=23, minute=59, second=0, microsecond=0)

def BeginningOfDay(dt):
    """Sets hour to 00h00:00.0 dt is datetime."""
    return dt.replace(hour=0, minute=0, second=0, microsecond=0)

def IntervalMax(ts, period):
    """Returns maximum timestamp that can be included in interval.
    
    The interval is jointly defined by ts and period.
    
    Each period requires a different method to calculate its end.
    
    TODO: all timestamp routines should be better grouped in a single API,
    there is stuff inside timedata.py"""
    if period <= qp.min1:
        return dt2ts(ts2dt(ts).replace(second=59))
    elif period < qp.daily:
        nm = qp.data[period]["nm"]
        dt = ts2dt(ts).replace(second=0)
        return dt2ts(dt.replace(minute=(dt.minute)//nm*nm))+nm*60-1
    elif period >= qp.daily:
        # Initial treatment
        ts = dt2ts(ts2dt(ts).replace(hour=23, minute=59, second=59))
        if period == qp.daily:
            return ts
        dt = ts2dt(ts)
        if period == qp.weekly:
            return ts+86400*(6-dt.weekday()) # End of Sunday
        else:
            if period > qp.monthly:
                nM = qp.data[period]["numMonths"]
                newMonth = (dt.month-1)//nM*nM+1+(nM-1)
#                print "newMonth is", newMonth
                dt = dt.replace(month=newMonth)
            #rolls to last day of month
            if dt.month == 12:
                return dt2ts(dt.replace(day=31))
            else: 
#                delta = datetime.timedelta(
                return dt2ts(dt.replace(month=dt.month+1, day=1))-86400


################################################################################

def Name2Symbol(s):
    """Takes <symbol> out of a string such as <supermarket>_<market>_<symbol>"""
    s = s[s.rfind("_")+1:]
    return s
    
def ReadSingleKeypress():
    """Waits for a single keypress on stdin.

    This is a silly function to call if you need to do it a lot because it has
    to store stdin's current setup, setup stdin for reading single keystrokes
    then read the single keystroke then revert stdin back after reading the
    keystroke.

    Returns the character of the key that was pressed (zero on
    KeyboardInterrupt which can happen when a signal gets handled)
    
    TODO: cite reference

    """
    import termios, fcntl, sys, os
    fd = sys.stdin.fileno()
    # save old state
    flags_save = fcntl.fcntl(fd, fcntl.F_GETFL)
    attrs_save = termios.tcgetattr(fd)
    # make raw - the way to do this comes from the termios(3) man page.
    attrs = list(attrs_save) # copy the stored version to update
    # iflag
    attrs[0] &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK 
                  | termios.ISTRIP | termios.INLCR | termios. IGNCR 
                  | termios.ICRNL | termios.IXON )
    # oflag
    attrs[1] &= ~termios.OPOST
    # cflag
    attrs[2] &= ~(termios.CSIZE | termios. PARENB)
    attrs[2] |= termios.CS8
    # lflag
    attrs[3] &= ~(termios.ECHONL | termios.ECHO | termios.ICANON
                  | termios.ISIG | termios.IEXTEN)
    termios.tcsetattr(fd, termios.TCSANOW, attrs)
    # turn off non-blocking
    fcntl.fcntl(fd, fcntl.F_SETFL, flags_save & ~os.O_NONBLOCK)
    # read a single keystroke
    try:
        ret = sys.stdin.read(1) # returns a single character
    except KeyboardInterrupt: 
        ret = 0
    finally:
        # restore old state
        termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save)
        fcntl.fcntl(fd, fcntl.F_SETFL, flags_save)
    return ret


def GetIPs():
    """Returns a list of IP addresses of the local machine.

    Solution by jeanthealmighty from thread available at
    http://forums.xkcd.com/viewtopic.php?f=11&t=27840

    """
    
    OS = os.name
    if OS == 'nt':
        ipconf = os.popen("ipconfig").read()
    elif OS == 'posix':
        ipconf = os.popen("ifconfig").read()
    else:
        raise("Cannot run on OS: %s" % OS)
    #print [x.strip() for x in ipconf.split("\n")]
    tIPs = re.findall("\d{1,3}[.]\d{1,3}[.]\d{1,3}[.]\d{1,3}", ipconf)
    IPs = []
    for IP in tIPs:
        if IP == "127.0.0.1":
            IPs.append(IP)
#            continue
        else:
            if IP.startswith("255") or IP.endswith("255") or IP.endswith(".1"):
                continue
            IPs.append(IP)
        
    return IPs
        

def AskUser():
    """
    Bonus function: text menu to select IP address from list.
    
    Returns chosen IP address or None if user decided not to select anything.
    """
    
    IPs = GetIPs()
    if len(IPs) == 1:
        print "Using IP %s" % IPs[0]
        return IPs[0]
    else:
        print "Please select IP address to bind to:"
        for (i, IP) in zip(range(1, len(IPs)+1), IPs):
            print "%1d - %s" % (i, IP)
        
        flagExit = False
        while not flagExit:
            try:
                i = input("? ")
                try:
                    if i < 0 or i > len(IPs):
                        print "Choice out of range."
                    else:
                        flagExit = True
                except:
                    print "Try again."
            except KeyboardInterrupt:
                raise
            except:
                print "Don't mess around."

        return IPs[i-1]


################################################################################
# Binary searches

from bisect import bisect_left

def BSearch(a, x, lo=0, hi=None):
    """Returns index of x in a, or -1 if x not in a.
    
    Arguments:
        a -- ordered numeric sequence
        x -- element to search within a
        lo -- lowest index to consider in search* 
        hi -- highest index to consider in search*
        
    *bisect.bisect_left capability that we don't need to loose."""
    if len(a) == 0: return -1
    hi = hi if hi is not None else len(a)
    pos = bisect_left(a, x, lo, hi)
    return pos if pos != hi and a[pos] == x else -1 # doesn't walk off the end


def BSearchRound(a, x, lo=0, hi=None):
    """Returns index of a that is closest to x.
    
    Arguments:
        a -- ordered numeric sequence
        x -- element to search within a
        lo -- lowest index to consider in search* 
        hi -- highest index to consider in search*
        
    *bisect.bisect_left capability that we don't need to loose."""
    if len(a) == 0: return -1
    hi = hi if hi is not None else len(a)
    pos = bisect_left(a, x, lo, hi)
    
    if pos >= hi:
        return hi-1
    elif a[pos] == x or pos == lo:
        return pos
    else:
        return pos-1 if x-a[pos-1] <= a[pos]-x else pos


def BSearchCeil(a, x, lo=0, hi=None):
    """Returns lowest i such as a[i] >= x, or -1 if x > all elements in a
    
    So, if x is in between two elements in a, this function will return the
    index of the higher element, hence "Ceil".
    
    Arguments:
        a -- ordered numeric sequence
        x -- element to search within a
        lo -- lowest index to consider in search
        hi -- highest index to consider in search"""
    if len(a) == 0: return -1
    hi = hi if hi is not None else len(a)
    pos = bisect_left(a, x, lo, hi)
    return pos if pos < hi else -1


def BSearchFloor(a, x, lo=0, hi=None):
    """Returns highest i such as a[i] <= x, or -1 if x < all elements in a
    
    So, if x is in between two elements in a, this function will return the
    index of the lower element, hence "Floor".
    
    Arguments:
        a -- ordered numeric sequence
        x -- element to search within a
        lo -- lowest index to consider in search
        hi -- highest index to consider in search"""
    if len(a) == 0: return -1
    hi = hi if hi is not None else len(a)
    pos = bisect_left(a, x, lo, hi)
    return pos-1 if pos >= hi\
           else (pos if x == a[pos] else (pos-1 if pos > lo else -1))



def slugify(value):
    """
    Converts to lowercase, removes non-alpha characters,
    and converts spaces to hyphens.
    
    Used for making file names.
    
    TODO: cite base work.
    """
    value = re.sub('[^\w\s.]', '', value).strip().lower()
    value = re.sub('[-\s]+', '-', value)
    return value

def GoodVarname(value):
    """
    Converts to lowercase, removes non-alpha characters,
    and converts spaces to hyphens.
    """
    value = re.sub('[^\w]', '', value).strip()
    if len(value) == 0:
        raise RuntimeError("Cannot make good varname, string has no valid characters")
    if value[0] in '0123456789':
        value = '_'+value # puts underscore if starts with number
    return value


def tail(F, lines_2find=50):
    """Returns last lines_2find lines from file object F.

    "Based on Eyecue answer (Jun 10 '10 at 21:28)" [1]

    Reference: 
      [1] http://stackoverflow.com/questions/136168/get-last-n-lines-of-a-file-with-python-similar-to-tail
    """
    F.seek(0, 2)                         #go to end of file
    bytes_in_file = F.tell()             
    lines_found, total_bytes_scanned = 0, 0
    while (lines_2find+1 > lines_found and
           bytes_in_file > total_bytes_scanned): 
        byte_block = min(1024, bytes_in_file-total_bytes_scanned)
        F.seek(-(byte_block+total_bytes_scanned), 2)
        total_bytes_scanned += byte_block
        lines_found += F.read(1024).count('\n')
    F.seek(-total_bytes_scanned, 2)
    line_list = list(F.readlines())
    ll = line_list[-lines_2find:]
    return "".join(ll)


def FormatEllapsed(seconds, message="time"):
    """Creates ellapsed time message."""
    return "ELLAPSED \"%s\" %.3g seconds" % (message, seconds)
    

####################################################################################
## Logging stuff

LOGFORMAT = '(%(name)s)%(levelname)s[%(asctime)s]:%(message)s'

def GetLogger(name, logFilename=None, flagStdout=True):
    """Creates a logger that logs to file and console simultaneously.
    
    Arguments:
        name -- logger name
        logFilename="<name>.log" -- filename to log into.
        flagStdout=True -- Whether to send to stdout apart from file
    """
    logFilename = logFilename if logFilename else name+".log"
    logger = logging.Logger(name=name, level=logging.DEBUG)
    fmtr = logging.Formatter(fmt=LOGFORMAT)
    ch = logging.FileHandler(filename=logFilename)
    ch.setFormatter(fmtr)
    logger.addHandler(ch)
    if flagStdout:
        ch = logging.StreamHandler(sys.stdout)
        ch.setFormatter(fmtr)
        logger.addHandler(ch)
    else:
        pass
    return logger


def CatchException(f):
    """Decorator to be put before class methods to automatically log exceptions.
    
    If the method class has a logger property, it will be used for logging,
    otherwise, the logging module will be used directly. The exception will be
    re-raised."""
    @functools.wraps(f)
    def func(self, *args, **kwargs):
        try:
            return f(self, *args, **kwargs)
        except Exception as E:
            s = "Error in method %s.%s" % (self.__class__.__name__, f.__name__)
            if hasattr(self, "logger"):
                self.logger.exception(s)
            else:
                logging.exception(s)
            raise
    return func


def CatchException2(f):
    """Same as CatchException but displays error dialog in box as well.
    
    If the method class has a logger property, it will be used for logging,
    otherwise, the logging module will be used directly. The exception will be
    re-raised."""
    @functools.wraps(f)
    def func(self, *args, **kwargs):
        try:
            return f(self, *args, **kwargs)
        except Exception as E:
            s = "Error in method %s.%s" % (self.__class__.__name__, f.__name__)
            if hasattr(self, "logger"):
                self.logger.exception(s)
            else:
                logging.exception(s)
                QMessageBox.critical(self, "Error", E.message)

            raise
    return func



def last_n(v, n):
    """Last n elements of vector, or less if vector is smaller than n."""
    return v if len(v) <= n else v[-n:]


def FormatCrosshairTuples(tuples):
    return "; ".join(["%s=<font color=red>%s</font>" % \
           (a, b) for (a, b) in tuples])



def isempty(x):
    """Returns True if x is None or empty sequence."""
    if hasattr(x, "__len__"):
        return x.__len__() == 0
    else:
        return x is None



